﻿/*
 * Copyright 2012 Nascent Digital (http://www.nascentdigital.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0 
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
using Nascent.GoogleAnalytics.Metrics;
using System;
using System.Collections.Generic;
using System.Globalization;


namespace Nascent.GoogleAnalytics
{
    /// <summary>
    /// Provides page, event and custom variable analytics tracking for an 
    /// application.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///     The <b>AnalyticsTracker</b> class instance can track page-level 
    ///     interactions for an application, reporting page load time and 
    ///     page-bound events/variables while a particular page is loaded.
    ///     </para>
    ///     <para>
    ///     A tracker must be obtained via the static <see cref="GetInstance()"/>
    ///     method, which returns a tracker for a specific web property id.
    ///     </para>
    ///     <para>
    ///     By default, a tracker is set to the application root path ("/"), 
    ///     which should be updated by calling <see cref="TrackPageView()"/>
    ///     before any other tracking methods are invoked.
    ///     </para>
    ///     <para>
    ///     Any public <b>static</b> (<b>Shared</b> in Visual Basic) members of 
    ///     this type are thread safe. Any instance members are not guaranteed 
    ///     to be thread safe.
    ///     </para>
    ///     <para>
    ///     All recorded metrics are sent to the analytics service in the order
    ///     that they are recorded, with retry attempts being made until the
    ///     service can be contacted.  The current implementation does not
    ///     support offline tracking or persistence of tracking metrics.
    ///     </para>
    /// </remarks>
    /// <thread
    public sealed class AnalyticsTracker : IDisposable
    {
        #region constants

        /// <summary>
        /// Defines the maximum number of custom variables that can be tracked,
        /// with the current value being <code>5</code>.
        /// </summary>
        public const int MaxCustomVariableIndex = 5;

        #endregion


        #region class variables

        private static readonly object _syncRoot;
        private static readonly IDictionary<string, AnalyticsService> _services;

        #endregion


        #region instance variables

        private readonly AnalyticsService _analyticsService;
        private string _currentPage;
        private volatile bool _disposed;

        #endregion


        #region constructors

        static AnalyticsTracker()
        {
            // initialize class variables
            _syncRoot = new object();
            _services = new Dictionary<string, AnalyticsService>();
        }

        private AnalyticsTracker(AnalyticsService analyticsService)
        {
            // initialize instance variables
            _analyticsService = analyticsService;
            _currentPage = "/";

            // retain sevice
            analyticsService.Retain();
        }

        #endregion


        #region destructors

        ~AnalyticsTracker()
        {
            Dispose(false);
        }

        #endregion


        #region properties

        /// <summary>
        /// The current web property id associated to this tracker.
        /// </summary>
        public string WebPropertyId
        {
            get { return _analyticsService.WebPropertyId; }
        }

        #endregion


        #region public methods

        /// <summary>
        /// Creates a new instance of a tracker, bound to the specified web
        /// property id.
        /// </summary>
        /// <param name="webPropertyId">
        /// A valid web property identifier, created by a Google Analytics 
        /// administrator.
        /// </param>
        public static AnalyticsTracker GetInstance(string webPropertyId)
        {
            // resolve analytics service
            AnalyticsService service = null;
            lock (_syncRoot)
            {
                // try to reuse existing service (or create one)
                if (_services.TryGetValue(webPropertyId, out service) == false)
                {
                    // create new service
                    service = new AnalyticsService(webPropertyId);

                    // store in tracker cache
                    _services.Add(webPropertyId, service);
                }
            }

            // create new analytics tracker
            AnalyticsTracker tracker = new AnalyticsTracker(service);

            // return tracker
            return tracker;
        }

        /// <summary>
        /// Sets a scope-bound custom variable for the tracker, which will be
        /// posted with the next tracked page view or event.
        /// </summary>
        /// <param name="index">
        /// The 1-based index of the value being set.
        /// </param>
        /// <param name="name">
        /// The name of this custom variable.
        /// </param>
        /// <param name="value">
        /// The value of this custom variable.
        /// </param>
        /// <param name="scope">
        /// The scope of this variable, which defaults to a page-level value.
        /// </param>
        /// <remarks>
        /// This method does not post the variables immediately, but instead
        /// requires a page view or event to be tracked so that the variable
        /// information can be sent to the analytics service.
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown if the variable index is not within the specified range of
        /// 1 to 5.
        /// </exception>
        public void SetCustomVariable(int index, string name, string value,
            VariableScope scope = VariableScope.Page)
        {
            // throw if index is out of range
            if (index < 1
                || index > MaxCustomVariableIndex)
            {
                throw new ArgumentOutOfRangeException("index", index,
                    string.Format(CultureInfo.InvariantCulture,
                        "Custom variable indices must be between 1 and {0}",
                        index));
            }

            // set variable
            _analyticsService.SetVariable(new Variable()
            {
                Index = index,
                Name = name, 
                Value = value, 
                Scope = scope
            });
        }

        /// <summary>
        /// Clears the variable at the specified index, finalizing the update
        /// on the next tracked page view or event.
        /// </summary>
        /// <param name="index">
        /// The 1-based index of the variable being cleared.
        /// </param>
        /// <remarks>
        /// This method does not clear the variables immediately, but instead
        /// requires a page view or event to be tracked so that the variable
        /// information can be sent to the analytics service.
        /// </remarks>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Thrown if the variable index is not within the specified range of
        /// 1 to 5.
        /// </exception>
        public void ClearCustomVariable(int index)
        {
            // throw if index is out of range
            if (index < 1
                || index > MaxCustomVariableIndex)
            {
                throw new ArgumentOutOfRangeException("index", index,
                    string.Format(CultureInfo.InvariantCulture,
                        "Custom variable indices must be between 1 and {0}",
                        index));
            }

            // clear value
            _analyticsService.ClearVariable(index);
        }

        /// <summary>
        /// Tracks a page view for the specified relative page path.
        /// </summary>
        /// <param name="page">
        /// The page path which has been loaded.
        /// </param>
        /// <remarks>
        ///     <para>
        ///     It is standard convention that a page path be a relative path 
        ///     from a conceptual application root, prefixed with a forward 
        ///     slash ("/").
        ///     </para>
        ///     <para>
        ///     It is important to note that a page will be tracked regardless
        ///     of the previous page path, even if the page path has not 
        ///     changed since the last invocation of this method.  Events 
        ///     will always be associated to the last page path tracked, or
        ///     the default path ("/") if no previous page view has been 
        ///     tracked.
        ///     </para>
        /// </remarks>
        /// <exception cref="ArgumentNullException">
        /// Thrown if the page path is null.
        /// </exception>
        public void TrackPageView(string page)
        {
            // throw if argument is invalid
            if (page == null)
            {
                throw new ArgumentNullException("path");
            }

            // update current page
            _currentPage = page;

            // record metric
            _analyticsService.RecordMetric(new PageView()
            {
                Page = page
            });
        }

        /// <summary>
        /// Tracks an event for the last page view path tracked, or for the 
        /// default page path ("/") if called before any page views have been
        /// recorded.
        /// </summary>
        /// <param name="category">
        /// The category of the event.
        /// </param>
        /// <param name="action">
        /// The action which the event represents.
        /// </param>
        /// <param name="label">
        /// An optional contextual label for the event.
        /// </param>
        public void TrackEvent(string category, string action, 
            string label = null)
        {
            TrackEvent(category, action, label, null);
        }

        /// <summary>
        /// Tracks an event for the last page view path tracked, or for the 
        /// default page path ("/") if called before any page views have been
        /// recorded.
        /// </summary>
        /// <param name="category">
        /// The category of the event.
        /// </param>
        /// <param name="action">
        /// The action which the event represents.
        /// </param>
        /// <param name="label">
        /// A contextual label for the event.
        /// </param>
        /// <param name="label">
        /// A numeric value associated to the event (will be aggregated by the 
        /// analytics service).
        /// </param>
        public void TrackEvent(string category, string action, int value)
        {
            TrackEvent(category, action, null, value);
        }

        /// <summary>
        /// Tracks an event for the last page view path tracked, or for the 
        /// default page path ("/") if called before any page views have been
        /// recorded.
        /// </summary>
        /// <param name="category">
        /// The category of the event.
        /// </param>
        /// <param name="action">
        /// The action which the event represents.
        /// </param>
        /// <param name="label">
        /// An optional contextual label for the event.
        /// </param>
        /// <param name="label">
        /// An optional numeric value associated to the event (will be 
        /// aggregated by the analytics service).
        /// </param>
        public void TrackEvent(string category, string action, 
            string label = null, int? value = null)
        {
            // record metric
            _analyticsService.RecordMetric(new PageEvent()
            {
                Page = _currentPage,
                Category = category, 
                Action = action,
                Label = label,
                Value = value
            });
        }

        #endregion


        #region helper methods


        #region IDisposable methods

        void IDisposable.Dispose()
        {
            Dispose(true);
        }

        private void Dispose(bool disposing)
        {
            if (_disposed == false)
            {
                // mark disposed
                _disposed = true;

                // stop tracking (only if disposing)
                if (disposing)
                {
                    _analyticsService.Release();
                }
            }
        }

        #endregion


        #endregion

    }  // class AnalyticsTracker

}  // namespace Nascent.GoogleAnalytics
